• repo
  • readme
  • stackblitz

vunapay

View on Github
.

vunapay
.
B

Languages

  • .
  • TypeScript100.0%

vunapay readme

VunaPay - Mini Wallet Transaction API

A simple backend service that manages wallet transactions, built with Express, SQLite, and Drizzle ORM.

Tech Stack

  • Runtime: Node.js + TypeScript (ESM)
  • Framework: Express 5
  • Database: SQLite via better-sqlite3
  • ORM: Drizzle ORM
  • Validation: Zod

Setup

Prerequisites

  • Node.js 20+
  • pnpm (or npm/yarn)

Install & Run

# Install dependencies
pnpm install

# Run database migrations
pnpm db:migrate

# Start the dev server (with hot reload)
pnpm dev

The API will be available at http://localhost:3000.

Seed Test Data

To populate the database with sample accounts:

curl -X POST http://localhost:3000/api/seed

This creates 3 accounts (Alice, Bob, Charlie) with pre-funded balances and resets the DB each time.

API Endpoints

Method Endpoint Description
GET /health Health check
POST /api/seed Seed DB with test data
GET /api/accounts List all accounts with balances
POST /api/accounts Create a new account
GET /api/accounts/:id Get account details
POST /api/accounts/:id/deposit Deposit funds
POST /api/accounts/:id/withdraw Withdraw funds
POST /api/transfers Transfer between accounts
GET /api/accounts/:id/transactions Transaction history (paginated)

Example Requests

Create Account

curl -X POST http://localhost:3000/api/accounts \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "alice@example.com"}'
{
  "success": true,
  "data": {
    "id": 1,
    "name": "Alice",
    "email": "alice@example.com",
    "balance": 0,
    "createdAt": "2026-03-15 12:00:00",
    "updatedAt": "2026-03-15 12:00:00"
  }
}

List All Accounts

curl http://localhost:3000/api/accounts

Deposit

curl -X POST http://localhost:3000/api/accounts/1/deposit \
  -H "Content-Type: application/json" \
  -d '{"amount": 150.00, "description": "Initial deposit", "reference": "dep-001"}'
{
  "success": true,
  "data": {
    "transaction": {
      "id": "uuid",
      "type": "deposit",
      "amount": 150,
      "senderId": null,
      "receiverId": 1,
      "reference": "dep-001",
      "description": "Initial deposit",
      "createdAt": "2026-03-15 12:00:00"
    },
    "newBalance": 150
  }
}

Withdraw

curl -X POST http://localhost:3000/api/accounts/1/withdraw \
  -H "Content-Type: application/json" \
  -d '{"amount": 50.00, "description": "ATM withdrawal", "reference": "wd-001"}'

Transfer

curl -X POST http://localhost:3000/api/transfers \
  -H "Content-Type: application/json" \
  -d '{"senderId": 1, "receiverId": 2, "amount": 50.00, "description": "Payment", "reference": "txn-001"}'
{
  "success": true,
  "data": {
    "transaction": {
      "id": "uuid",
      "type": "transfer",
      "amount": 50,
      "senderId": 1,
      "receiverId": 2,
      "reference": "txn-001",
      "description": "Payment",
      "createdAt": "2026-03-15 12:00:00"
    },
    "senderBalance": 100,
    "receiverBalance": 50
  }
}

Transaction History (Paginated)

curl "http://localhost:3000/api/accounts/1/transactions?page=1&limit=10"

Design Decisions

Money as Integers (Cents)

All monetary values are stored internally as integers in cents to avoid floating-point precision issues. The API accepts and returns decimal dollar values (e.g., 150.00), converting at the service layer boundary.

Idempotency via Reference

Every transaction requires a unique reference string. If a client retries a request with the same reference, the API returns a 409 Conflict instead of creating a duplicate transaction.

Atomicity

Transfers use SQLite transactions to ensure debit + credit + transaction record creation are all-or-nothing. If any step fails, the entire operation rolls back.

Auto-Increment Account IDs

Account IDs are auto-incrementing integers for ease of manual testing (/api/accounts/1 vs UUID strings).

Error Handling

All error responses follow a consistent format:

{
  "success": false,
  "error": "Description of what went wrong"
}
Status Condition
400 Invalid input / validation error
404 Account not found
409 Duplicate reference
422 Insufficient balance
500 Unexpected server error

Project Structure

hljs src/
  index.ts                    # Express app entry point
  config/
    database.ts               # Drizzle + SQLite setup
    env.ts                    # Environment config
    migrate.ts                # Migration runner
  db/schema/
    index.ts                  # Drizzle table definitions
  types/                      # TypeScript DTOs and interfaces
  repositories/               # Database access layer
  services/                   # Business logic layer
  controllers/                # HTTP request handlers
  routes/                     # Express route definitions
  middleware/                  # Validation & error handling
  utils/                      # Helpers (errors, money conversion)

Follows Clean Architecture: Controllers (HTTP) -> Services (business logic) -> Repositories (DB).

Scope & Trade-offs

This is a minimal implementation focused on the core task requirements: account management, deposits, withdrawals, transfers, and transaction history with idempotency and atomicity guarantees.

The following were intentionally left out to keep scope tight:

  • Authentication & Authorization — No auth layer (JWT, API keys, sessions). All endpoints are open. In production, every mutation would require an authenticated identity and ownership checks.
  • Rate Limiting — No request throttling. A production deployment would need rate limiting per IP/account to prevent abuse on financial endpoints.
  • Logging & Observability — No structured logging, request tracing, or metrics. Only console.error on unhandled exceptions.
  • Input Sanitization — Zod handles schema validation, but there is no additional sanitization (e.g., XSS, SQL injection beyond parameterized queries).
  • Pagination Defaults — The max page size is capped at 100 but there are no cursor-based pagination or sort options.
  • Audit Trail — Transactions are immutable records, but there is no separate audit log for admin actions, failed attempts, or account changes.
  • Deployment Configuration — No Dockerfile, CI pipeline, or environment-specific configs beyond a basic .env.
  • 100% Test Coverage — Tests cover the critical paths (happy, failure, edge, atomicity) but do not aim for exhaustive branch coverage on every utility and middleware.